D:\a\scloud-dns\scloud-dns\src\config.rs
Line | Count | Source |
1 | | //! Configuration types for scloud-dns |
2 | | //! |
3 | | //! This file contains Serde (Deserialize/Serialize) structs that map to the |
4 | | //! JSON configuration you provided. It includes helpers to load the config |
5 | | //! from a file and a light `validate()` method placeholder you can extend. |
6 | | |
7 | | use crate::exceptions::SCloudException; |
8 | | use anyhow::{Context, Result}; |
9 | | use serde::{Deserialize, Serialize}; |
10 | | use std::collections::HashSet; |
11 | | use std::fs; |
12 | | use std::path::Path; |
13 | | |
14 | | /// Top-level configuration |
15 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
16 | | pub struct Config { |
17 | | #[serde(default)] |
18 | | pub server: ServerConfig, |
19 | | |
20 | | #[serde(default)] |
21 | | pub workers: WorkersConfig, |
22 | | |
23 | | #[serde(default)] |
24 | | pub logging: LoggingConfig, |
25 | | |
26 | | #[serde(default)] |
27 | | pub metrics: MetricsConfig, |
28 | | |
29 | | #[serde(default)] |
30 | | pub admin: AdminConfig, |
31 | | |
32 | | #[serde(default)] |
33 | | pub acl: Vec<AclEntry>, |
34 | | |
35 | | #[serde(default)] |
36 | | pub listener: Vec<ListenerConfig>, |
37 | | |
38 | | #[serde(default)] |
39 | | pub doh: DohConfig, |
40 | | |
41 | | #[serde(default)] |
42 | | pub forwarder: Vec<ForwarderConfig>, |
43 | | |
44 | | #[serde(default)] |
45 | | pub root_hints: RootHintsConfig, |
46 | | |
47 | | #[serde(default)] |
48 | | pub cache: CacheConfig, |
49 | | |
50 | | #[serde(default)] |
51 | | pub recursion: RecursionConfig, |
52 | | |
53 | | #[serde(default)] |
54 | | pub ratelimit: RateLimitConfig, |
55 | | |
56 | | #[serde(default)] |
57 | | pub zone: Vec<ZoneConfig>, |
58 | | |
59 | | #[serde(default)] |
60 | | pub tsig_key: Vec<TsigKey>, |
61 | | |
62 | | #[serde(default)] |
63 | | pub axfr: AxfrConfig, |
64 | | |
65 | | #[serde(default)] |
66 | | pub dnssec: DnssecConfig, |
67 | | |
68 | | #[serde(default)] |
69 | | pub policy: PolicyConfig, |
70 | | |
71 | | #[serde(default)] |
72 | | pub amplification_mitigation: AmplificationMitigationConfig, |
73 | | |
74 | | #[serde(default)] |
75 | | pub tuning: TuningConfig, |
76 | | |
77 | | #[serde(default)] |
78 | | pub view: Vec<ViewConfig>, |
79 | | |
80 | | #[serde(default)] |
81 | | pub monitoring: MonitoringConfig, |
82 | | |
83 | | #[serde(default)] |
84 | | pub dynupdate: Vec<DynUpdateConfig>, |
85 | | |
86 | | #[serde(default)] |
87 | | pub limits: LimitsConfig, |
88 | | } |
89 | | |
90 | | impl Config { |
91 | | /// Load config from a JSON file path |
92 | 9 | pub fn from_file(path: &Path) -> Result<Self, SCloudException> { |
93 | 9 | let s = fs::read_to_string(path) |
94 | 9 | .with_context(|| format!0 ("reading config file {}", path0 .display0 ())) |
95 | 9 | .map_err(|_| SCloudException::SCLOUD_CONFIG_FILE_NOT_FOUND)?0 ; |
96 | 9 | let cfg: Config = serde_json::from_str(&s) |
97 | 9 | .context("parsing JSON config") |
98 | 9 | .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_JSON)?0 ; |
99 | 9 | cfg.validate()?0 ; |
100 | 9 | Ok(cfg) |
101 | 9 | } |
102 | | |
103 | | /// Validation hook |
104 | 10 | pub fn validate(&self) -> Result<(), SCloudException> { |
105 | 24 | let acl_names10 : HashSet<&str>10 = self.acl.iter()10 .map10 (|a| a.name.as_str()).collect10 (); |
106 | 16 | let tsig_names10 : HashSet<&str>10 = self.tsig_key.iter()10 .map10 (|t| t.name.as_str()).collect10 (); |
107 | 10 | let _forwarder_names: HashSet<&str> = |
108 | 24 | self.forwarder.iter()10 .map10 (|f| f.name.as_str()).collect10 (); |
109 | | |
110 | 72 | let is_acl_ref_valid10 = |s: &str| -> bool { |
111 | 72 | if s.trim().is_empty() { |
112 | 0 | return false; |
113 | 72 | } |
114 | 72 | acl_names.contains(s) || s16 .contains16 ('/') |
115 | 72 | }; |
116 | | |
117 | 10 | if self.server.bind_port == 0 { |
118 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_SERVER_PORT); |
119 | 10 | } |
120 | 10 | if self.server.max_udp_payload == 0 || self.server.max_udp_payload > 65535 { |
121 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_MAX_UDP_PAYLOAD); |
122 | 10 | } |
123 | 10 | if self.tuning.max_label_length == 0 || self.tuning.max_label_length > 63 { |
124 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS); |
125 | 10 | } |
126 | 10 | if self.tuning.max_domain_length == 0 || self.tuning.max_domain_length > 253 { |
127 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS); |
128 | 10 | } |
129 | 10 | if self.limits.max_udp_packet_size == 0 || self.limits.max_udp_packet_size > 65535 { |
130 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS); |
131 | 10 | } |
132 | | |
133 | 10 | let mut listener_names = HashSet::new(); |
134 | 24 | for l in &self.listener10 { |
135 | 24 | if l.name.trim().is_empty() { |
136 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER); |
137 | 24 | } |
138 | 24 | if !listener_names.insert(l.name.as_str()) { |
139 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_LISTENER_NAME); |
140 | 24 | } |
141 | 24 | if l.port == 0 { |
142 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PORT); |
143 | 24 | } |
144 | 24 | if l.protocols.is_empty() { |
145 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PROTOCOLS); |
146 | 24 | } |
147 | 24 | if !l.acl.trim().is_empty() && !is_acl_ref_valid(&l.acl) { |
148 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
149 | 24 | } |
150 | | |
151 | 24 | if l.enable_tls.unwrap_or(false) { |
152 | 8 | if l.tls_cert_path.as_deref().unwrap_or("").trim().is_empty() { |
153 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT); |
154 | 8 | } |
155 | 8 | if l.tls_key_path.as_deref().unwrap_or("").trim().is_empty() { |
156 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY); |
157 | 8 | } |
158 | 8 | if !l.protocols.iter().any(|p| matches!(p, Protocol::TCP)) { |
159 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_REQUIRES_TCP); |
160 | 8 | } |
161 | 16 | } |
162 | | } |
163 | | |
164 | 10 | if self.doh.enabled { |
165 | 8 | if self |
166 | 8 | .doh |
167 | 8 | .tls_cert_path |
168 | 8 | .as_deref() |
169 | 8 | .unwrap_or("") |
170 | 8 | .trim() |
171 | 8 | .is_empty() |
172 | | { |
173 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT); |
174 | 8 | } |
175 | 8 | if self |
176 | 8 | .doh |
177 | 8 | .tls_key_path |
178 | 8 | .as_deref() |
179 | 8 | .unwrap_or("") |
180 | 8 | .trim() |
181 | 8 | .is_empty() |
182 | | { |
183 | 0 | return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY); |
184 | 8 | } |
185 | 8 | if self.doh.paths.is_empty() { |
186 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DOH); |
187 | 8 | } |
188 | 2 | } |
189 | | |
190 | 10 | if self.recursion.enabled { |
191 | 8 | if self.recursion.allowed_acl.trim().is_empty() { |
192 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
193 | 8 | } |
194 | 8 | if !is_acl_ref_valid(&self.recursion.allowed_acl) { |
195 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
196 | 8 | } |
197 | 2 | } |
198 | | |
199 | 10 | let mut fwd_names = HashSet::new(); |
200 | 24 | for f in &self.forwarder10 { |
201 | 24 | if f.name.trim().is_empty() { |
202 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER); |
203 | 24 | } |
204 | 24 | if !fwd_names.insert(f.name.as_str()) { |
205 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_FORWARDER_NAME); |
206 | 24 | } |
207 | 24 | if f.addresses.is_empty() { |
208 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER); |
209 | 24 | } |
210 | 40 | for a in &f.addresses24 { |
211 | 40 | if a.parse::<std::net::SocketAddr>().is_err() { |
212 | 0 | return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR); |
213 | 40 | } |
214 | | } |
215 | | } |
216 | | |
217 | 10 | let mut zone_names = HashSet::new(); |
218 | 32 | for z in &self.zone10 { |
219 | 32 | if z.name.trim().is_empty() { |
220 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_ZONE); |
221 | 32 | } |
222 | 32 | if !zone_names.insert(z.name.as_str()) { |
223 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_ZONE_NAME); |
224 | 32 | } |
225 | | |
226 | 32 | match z.kind { |
227 | | ZoneType::Master => { |
228 | 16 | let inline = z.inline.unwrap_or(false); |
229 | 16 | if inline { |
230 | 8 | if z.records.is_empty() { |
231 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE); |
232 | 8 | } |
233 | 8 | let has_soa = z |
234 | 8 | .records |
235 | 8 | .iter() |
236 | 8 | .any(|r| r.r#type.eq_ignore_ascii_case("SOA")); |
237 | 8 | if !has_soa { |
238 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE); |
239 | 8 | } |
240 | | } else { |
241 | 8 | if z.file.as_deref().unwrap_or("").trim().is_empty() { |
242 | 0 | return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE); |
243 | 8 | } |
244 | | } |
245 | | |
246 | 16 | if let Some(acl8 ) = z.notify_acl.as_deref() { |
247 | 8 | if !acl.trim().is_empty() && !is_acl_ref_valid(acl) { |
248 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
249 | 8 | } |
250 | 8 | } |
251 | 16 | if let Some(acl8 ) = z.allow_transfer_acl.as_deref() { |
252 | 8 | if !acl.trim().is_empty() && !is_acl_ref_valid(acl) { |
253 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
254 | 8 | } |
255 | 8 | } |
256 | | |
257 | 16 | if let Some(k8 ) = z.axfr_tsig_key.as_deref() { |
258 | 8 | if !k.trim().is_empty() && !tsig_names.contains(k) { |
259 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY); |
260 | 8 | } |
261 | 8 | } |
262 | | } |
263 | | ZoneType::Slave => { |
264 | 8 | if z.masters.is_empty() { |
265 | 0 | return Err(SCloudException::SCLOUD_CONFIG_SLAVE_MISSING_MASTERS); |
266 | 8 | } |
267 | 8 | for m in &z.masters { |
268 | 8 | if m.parse::<std::net::SocketAddr>().is_err() { |
269 | 0 | return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR); |
270 | 8 | } |
271 | | } |
272 | 8 | if z.file.as_deref().unwrap_or("").trim().is_empty() { |
273 | 0 | return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE); |
274 | 8 | } |
275 | | } |
276 | | ZoneType::Forward => { |
277 | 8 | if z.forwarders.is_empty() { |
278 | 0 | return Err(SCloudException::SCLOUD_CONFIG_FORWARD_ZONE_MISSING_FORWARDERS); |
279 | 8 | } |
280 | 8 | for f in &z.forwarders { |
281 | 8 | if f.parse::<std::net::SocketAddr>().is_err() { |
282 | 0 | return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR); |
283 | 8 | } |
284 | | } |
285 | | } |
286 | 0 | ZoneType::Stub => { |
287 | 0 | // TODO: not defined JSON yet, strict checks later when I will implement it. |
288 | 0 | } |
289 | | } |
290 | | |
291 | 48 | for r in &z.records32 { |
292 | 48 | if r.r#type.eq_ignore_ascii_case("MX") { |
293 | 8 | if r.priority.is_none() { |
294 | 0 | return Err(SCloudException::SCLOUD_CONFIG_MX_MISSING_PRIORITY); |
295 | 8 | } |
296 | 40 | } else if r.priority.is_some() { |
297 | 0 | return Err(SCloudException::SCLOUD_CONFIG_PRIORITY_ON_NON_MX); |
298 | 40 | } |
299 | | } |
300 | | } |
301 | | |
302 | 10 | let mut view_names = HashSet::new(); |
303 | 16 | for v in &self.view10 { |
304 | 16 | if v.name.trim().is_empty() { |
305 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW); |
306 | 16 | } |
307 | 16 | if !view_names.insert(v.name.as_str()) { |
308 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_VIEW_NAME); |
309 | 16 | } |
310 | 16 | if v.acl.trim().is_empty() || !is_acl_ref_valid(&v.acl) { |
311 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
312 | 16 | } |
313 | 16 | for vz in &v.zones { |
314 | 16 | if vz.name.trim().is_empty() || vz.file.trim().is_empty() { |
315 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW); |
316 | 16 | } |
317 | | } |
318 | | } |
319 | | |
320 | 10 | for d8 in &self.dynupdate { |
321 | 8 | if d.zone.trim().is_empty() { |
322 | 0 | return Err(SCloudException::SCLOUD_CONFIG_INVALID_DYNUPDATE); |
323 | 8 | } |
324 | 8 | if d.acl.trim().is_empty() || !is_acl_ref_valid(&d.acl) { |
325 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE); |
326 | 8 | } |
327 | 8 | if let Some(k) = d.tsig_key.as_deref() { |
328 | 8 | if !k.trim().is_empty() && !tsig_names.contains(k) { |
329 | 0 | return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY); |
330 | 8 | } |
331 | 0 | } |
332 | | |
333 | 8 | if !zone_names.contains(d.zone.as_str()) { |
334 | 0 | return Err(SCloudException::SCLOUD_CONFIG_DYNUPDATE_UNKNOWN_ZONE); |
335 | 8 | } |
336 | | } |
337 | | |
338 | 10 | Ok(()) |
339 | 10 | } |
340 | | |
341 | | /// Get the address of a specific forwarder by index value |
342 | | #[allow(unused)] |
343 | 5 | pub(crate) fn try_get_forwarder_addr_by_index( |
344 | 5 | &self, |
345 | 5 | forwarder_index: usize, |
346 | 5 | address_index: usize, |
347 | 5 | ) -> Result<std::net::SocketAddr, SCloudException> { |
348 | 5 | let addr = self |
349 | 5 | .forwarder |
350 | 5 | .get(forwarder_index) |
351 | 5 | .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)?0 |
352 | | .addresses |
353 | 5 | .get(address_index) |
354 | 5 | .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_ADDRESS)?0 |
355 | 5 | .parse() |
356 | 5 | .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR)?0 ; |
357 | | |
358 | 5 | Ok(addr) |
359 | 5 | } |
360 | | |
361 | | // TODO: add a loop to test the next address for each retry |
362 | 5 | pub(crate) fn try_get_forwarder_addr_by_name( |
363 | 5 | &self, |
364 | 5 | forwarder_name: &str, |
365 | 5 | ) -> Result<std::net::SocketAddr, SCloudException> { |
366 | 5 | let forwarder = self |
367 | 5 | .forwarder |
368 | 5 | .iter() |
369 | 12 | .find5 (|f| f.name == forwarder_name) |
370 | 5 | .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)?0 ; |
371 | | |
372 | 5 | for addr_str in &forwarder.addresses { |
373 | 5 | if let Ok(addr) = addr_str.parse::<std::net::SocketAddr>() { |
374 | 5 | return Ok(addr); |
375 | 0 | } |
376 | | } |
377 | | |
378 | 0 | Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR) |
379 | 5 | } |
380 | | } |
381 | | |
382 | | impl Default for Config { |
383 | 5 | fn default() -> Self { |
384 | 5 | Self { |
385 | 5 | server: ServerConfig::default(), |
386 | 5 | workers: WorkersConfig::default(), |
387 | 5 | logging: LoggingConfig::default(), |
388 | 5 | metrics: MetricsConfig::default(), |
389 | 5 | admin: AdminConfig::default(), |
390 | 5 | acl: Vec::new(), |
391 | 5 | listener: Vec::new(), |
392 | 5 | doh: DohConfig::default(), |
393 | 5 | forwarder: Vec::new(), |
394 | 5 | root_hints: RootHintsConfig::default(), |
395 | 5 | cache: CacheConfig::default(), |
396 | 5 | recursion: RecursionConfig::default(), |
397 | 5 | ratelimit: RateLimitConfig::default(), |
398 | 5 | zone: Vec::new(), |
399 | 5 | tsig_key: Vec::new(), |
400 | 5 | axfr: AxfrConfig::default(), |
401 | 5 | dnssec: DnssecConfig::default(), |
402 | 5 | policy: PolicyConfig::default(), |
403 | 5 | amplification_mitigation: AmplificationMitigationConfig::default(), |
404 | 5 | tuning: TuningConfig::default(), |
405 | 5 | view: Vec::new(), |
406 | 5 | monitoring: MonitoringConfig::default(), |
407 | 5 | dynupdate: Vec::new(), |
408 | 5 | limits: LimitsConfig::default(), |
409 | 5 | } |
410 | 5 | } |
411 | | } |
412 | | |
413 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
414 | | pub struct ServerConfig { |
415 | | pub name: String, |
416 | | pub version: String, |
417 | | pub environment: String, |
418 | | pub max_concurrent_requests: usize, |
419 | | pub graceful_shutdown_timeout_secs: u64, |
420 | | |
421 | | pub default_ttl: u32, |
422 | | pub max_udp_payload: usize, |
423 | | pub enable_edns: bool, |
424 | | pub enable_tcp: bool, |
425 | | pub enable_dnssec: bool, |
426 | | |
427 | | pub bind_port: u16, |
428 | | } |
429 | | |
430 | | impl Default for ServerConfig { |
431 | 6 | fn default() -> Self { |
432 | 6 | ServerConfig { |
433 | 6 | name: "scloud-dns".to_string(), |
434 | 6 | version: "none".to_string(), |
435 | 6 | environment: "production".to_string(), |
436 | 6 | max_concurrent_requests: 5000, |
437 | 6 | graceful_shutdown_timeout_secs: 15, |
438 | 6 | default_ttl: 3600, |
439 | 6 | max_udp_payload: 4096, |
440 | 6 | enable_edns: true, |
441 | 6 | enable_tcp: true, |
442 | 6 | enable_dnssec: false, |
443 | 6 | bind_port: 53, |
444 | 6 | } |
445 | 6 | } |
446 | | } |
447 | | |
448 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
449 | | pub struct WorkersConfig { |
450 | | pub listener: u16, |
451 | | pub decoder: u16, |
452 | | pub query_dispatcher: u16, |
453 | | pub cache_lookup: u16, |
454 | | pub zone_manager: u16, |
455 | | pub resolver: u16, |
456 | | pub cache_writer: u16, |
457 | | pub encoder: u16, |
458 | | pub sender: u16, |
459 | | pub cache_janitor: u16, |
460 | | pub metrics: u16, |
461 | | pub tcp_acceptor: u16, |
462 | | } |
463 | | |
464 | | impl Default for WorkersConfig { |
465 | 5 | fn default() -> Self { |
466 | 5 | WorkersConfig { |
467 | 5 | listener: 5, |
468 | 5 | decoder: 5, |
469 | 5 | query_dispatcher: 3, |
470 | 5 | cache_lookup: 3, |
471 | 5 | zone_manager: 1, |
472 | 5 | resolver: 5, |
473 | 5 | cache_writer: 1, |
474 | 5 | encoder: 5, |
475 | 5 | sender: 5, |
476 | 5 | cache_janitor: 1, |
477 | 5 | metrics: 2, |
478 | 5 | tcp_acceptor: 1, |
479 | 5 | } |
480 | 5 | } |
481 | | } |
482 | | |
483 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
484 | | pub struct LoggingConfig { |
485 | | pub level: LogLevel, |
486 | | pub format: LogFormat, |
487 | | pub file: String, |
488 | | pub rotate: bool, |
489 | | pub live_print: bool, |
490 | | pub max_size_mb: u64, |
491 | | } |
492 | | |
493 | | impl Default for LoggingConfig { |
494 | 5 | fn default() -> Self { |
495 | 5 | LoggingConfig { |
496 | 5 | level: LogLevel::INFO, |
497 | 5 | format: LogFormat::TEXT, |
498 | 5 | file: "/var/log/scloud-dns/scloud-dns.log".to_string(), |
499 | 5 | rotate: true, |
500 | 5 | live_print: false, |
501 | 5 | max_size_mb: 200, |
502 | 5 | } |
503 | 5 | } |
504 | | } |
505 | | |
506 | | #[allow(non_camel_case_types)] |
507 | | #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] |
508 | | #[serde(rename_all = "lowercase")] |
509 | | pub enum LogLevel { |
510 | | TRACE = 0, |
511 | | DEBUG = 1, |
512 | | INFO = 2, |
513 | | WARN = 3, |
514 | | ERROR = 4, |
515 | | FATAL = 5, |
516 | | } |
517 | | |
518 | | impl LogLevel { |
519 | 0 | pub fn parse(s: &str) -> Self { |
520 | 0 | match s.to_ascii_lowercase().as_str() { |
521 | 0 | "trace" => Self::TRACE, |
522 | 0 | "debug" => Self::DEBUG, |
523 | 0 | "info" => Self::INFO, |
524 | 0 | "warn" | "warning" => Self::WARN, |
525 | 0 | "error" => Self::ERROR, |
526 | 0 | "fatal" => Self::FATAL, |
527 | 0 | _ => Self::WARN, |
528 | | } |
529 | 0 | } |
530 | | |
531 | 25 | pub(crate) fn as_str(self) -> &'static str { |
532 | 25 | match self { |
533 | 1 | Self::TRACE => "trace", |
534 | 19 | Self::DEBUG => "debug", |
535 | 4 | Self::INFO => "info", |
536 | 0 | Self::WARN => "warn", |
537 | 1 | Self::ERROR => "error", |
538 | 0 | Self::FATAL => "fatal", |
539 | | } |
540 | 25 | } |
541 | | } |
542 | | |
543 | | #[allow(non_camel_case_types)] |
544 | | #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] |
545 | | #[serde(rename_all = "lowercase")] |
546 | | pub enum LogFormat { |
547 | | JSON, |
548 | | TEXT, |
549 | | } |
550 | | |
551 | | impl LogFormat { |
552 | 0 | pub fn parse(s: &str) -> Self { |
553 | 0 | match s.to_ascii_lowercase().as_str() { |
554 | 0 | "json" => Self::JSON, |
555 | 0 | _ => Self::TEXT, |
556 | | } |
557 | 0 | } |
558 | | } |
559 | | |
560 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
561 | | pub struct MetricsConfig { |
562 | | pub enabled: bool, |
563 | | pub prometheus_bind: String, |
564 | | pub enable_health_endpoint: bool, |
565 | | pub health_bind: String, |
566 | | } |
567 | | |
568 | | impl Default for MetricsConfig { |
569 | 5 | fn default() -> Self { |
570 | 5 | MetricsConfig { |
571 | 5 | enabled: true, |
572 | 5 | prometheus_bind: "0.0.0.0:9153".to_string(), |
573 | 5 | enable_health_endpoint: true, |
574 | 5 | health_bind: "127.0.0.1:8081".to_string(), |
575 | 5 | } |
576 | 5 | } |
577 | | } |
578 | | |
579 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
580 | | pub struct AdminConfig { |
581 | | pub enabled: bool, |
582 | | pub bind: String, |
583 | | pub auth_token: String, |
584 | | pub enable_tls: bool, |
585 | | } |
586 | | |
587 | | impl Default for AdminConfig { |
588 | 5 | fn default() -> Self { |
589 | 5 | AdminConfig { |
590 | 5 | enabled: true, |
591 | 5 | bind: "127.0.0.1:8053".to_string(), |
592 | 5 | auth_token: "replace-with-secure-token".to_string(), |
593 | 5 | enable_tls: false, |
594 | 5 | } |
595 | 5 | } |
596 | | } |
597 | | |
598 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
599 | | pub struct AclEntry { |
600 | | pub name: String, |
601 | | pub networks: Vec<String>, // CIDRs or single IPs; parse later with ipnet or similar |
602 | | } |
603 | | |
604 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
605 | | pub struct ListenerConfig { |
606 | | pub name: String, |
607 | | pub address: String, |
608 | | pub port: u16, |
609 | | #[serde(default)] |
610 | | pub protocols: Vec<Protocol>, |
611 | | #[serde(default)] |
612 | | pub recursion_allowed: bool, |
613 | | /// ACL name or a raw CIDR/list string |
614 | | #[serde(default)] |
615 | | pub acl: String, |
616 | | #[serde(default)] |
617 | | pub workers: Option<usize>, |
618 | | #[serde(default)] |
619 | | pub enable_tls: Option<bool>, |
620 | | #[serde(default)] |
621 | | pub tls_cert_path: Option<String>, |
622 | | #[serde(default)] |
623 | | pub tls_key_path: Option<String>, |
624 | | } |
625 | | |
626 | | impl Default for ListenerConfig { |
627 | 1 | fn default() -> Self { |
628 | 1 | ListenerConfig { |
629 | 1 | name: String::new(), |
630 | 1 | address: "0.0.0.0".to_string(), |
631 | 1 | port: 53, |
632 | 1 | protocols: vec![Protocol::UDP], |
633 | 1 | recursion_allowed: false, |
634 | 1 | acl: "0.0.0.0/0".to_string(), |
635 | 1 | workers: None, |
636 | 1 | enable_tls: None, |
637 | 1 | tls_cert_path: None, |
638 | 1 | tls_key_path: None, |
639 | 1 | } |
640 | 1 | } |
641 | | } |
642 | | |
643 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
644 | | #[serde(rename_all = "lowercase")] |
645 | | pub enum Protocol { |
646 | | UDP, |
647 | | TCP, |
648 | | } |
649 | | |
650 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
651 | | pub struct DohConfig { |
652 | | pub enabled: bool, |
653 | | pub bind: String, |
654 | | #[serde(default)] |
655 | | pub tls_cert_path: Option<String>, |
656 | | #[serde(default)] |
657 | | pub tls_key_path: Option<String>, |
658 | | #[serde(default)] |
659 | | pub paths: Vec<String>, |
660 | | #[serde(default)] |
661 | | pub allowed_origins: Vec<String>, |
662 | | } |
663 | | |
664 | | impl Default for DohConfig { |
665 | 6 | fn default() -> Self { |
666 | 6 | DohConfig { |
667 | 6 | enabled: false, |
668 | 6 | bind: "0.0.0.0:443".to_string(), |
669 | 6 | tls_cert_path: None, |
670 | 6 | tls_key_path: None, |
671 | 6 | paths: vec!["/dns-query".to_string()], |
672 | 6 | allowed_origins: Vec::new(), |
673 | 6 | } |
674 | 6 | } |
675 | | } |
676 | | |
677 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
678 | | pub struct ForwarderConfig { |
679 | | pub name: String, |
680 | | pub addresses: Vec<String>, |
681 | | pub policy: ForwardPolicy, |
682 | | pub timeout_ms: u64, |
683 | | pub edns: bool, |
684 | | pub use_tcp_on_retry: Option<bool>, |
685 | | } |
686 | | |
687 | | impl Default for ForwarderConfig { |
688 | 1 | fn default() -> Self { |
689 | 1 | ForwarderConfig { |
690 | 1 | name: String::new(), |
691 | 1 | addresses: Vec::new(), |
692 | 1 | policy: ForwardPolicy::First, |
693 | 1 | timeout_ms: 1500, |
694 | 1 | edns: true, |
695 | 1 | use_tcp_on_retry: Some(true), |
696 | 1 | } |
697 | 1 | } |
698 | | } |
699 | | |
700 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
701 | | #[serde(rename_all = "snake_case")] |
702 | | #[derive(PartialEq)] |
703 | | pub enum ForwardPolicy { |
704 | | RoundRobin, |
705 | | First, |
706 | | Random, |
707 | | } |
708 | | |
709 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
710 | | pub struct RootHintsConfig { |
711 | | pub file: String, |
712 | | } |
713 | | |
714 | | impl Default for RootHintsConfig { |
715 | 5 | fn default() -> Self { |
716 | 5 | RootHintsConfig { |
717 | 5 | file: "/etc/scloud/root.hints".to_string(), |
718 | 5 | } |
719 | 5 | } |
720 | | } |
721 | | |
722 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
723 | | pub struct CacheConfig { |
724 | | pub enabled: bool, |
725 | | pub max_entries: usize, |
726 | | pub max_ttl_seconds: u64, |
727 | | pub negative_ttl_seconds: u64, |
728 | | pub eviction_policy: String, |
729 | | } |
730 | | |
731 | | impl Default for CacheConfig { |
732 | 6 | fn default() -> Self { |
733 | 6 | CacheConfig { |
734 | 6 | enabled: true, |
735 | 6 | max_entries: 200_000, |
736 | 6 | max_ttl_seconds: 86_400, |
737 | 6 | negative_ttl_seconds: 300, |
738 | 6 | eviction_policy: "lru".to_string(), |
739 | 6 | } |
740 | 6 | } |
741 | | } |
742 | | |
743 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
744 | | pub struct RecursionConfig { |
745 | | pub enabled: bool, |
746 | | pub allowed_acl: String, |
747 | | pub max_recursive_queries: usize, |
748 | | pub recursion_timeout_ms: u64, |
749 | | pub retry_interval_ms: u64, |
750 | | } |
751 | | |
752 | | impl Default for RecursionConfig { |
753 | 6 | fn default() -> Self { |
754 | 6 | RecursionConfig { |
755 | 6 | enabled: false, |
756 | 6 | allowed_acl: "internal".to_string(), |
757 | 6 | max_recursive_queries: 50, |
758 | 6 | recursion_timeout_ms: 5000, |
759 | 6 | retry_interval_ms: 200, |
760 | 6 | } |
761 | 6 | } |
762 | | } |
763 | | |
764 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
765 | | pub struct RateLimitConfig { |
766 | | pub enabled: bool, |
767 | | pub global_qps: u64, |
768 | | pub per_ip_qps: u64, |
769 | | pub per_subnet_qps: u64, |
770 | | pub rrl: RrlConfig, |
771 | | } |
772 | | |
773 | | impl Default for RateLimitConfig { |
774 | 6 | fn default() -> Self { |
775 | 6 | RateLimitConfig { |
776 | 6 | enabled: true, |
777 | 6 | global_qps: 3000, |
778 | 6 | per_ip_qps: 100, |
779 | 6 | per_subnet_qps: 1000, |
780 | 6 | rrl: RrlConfig::default(), |
781 | 6 | } |
782 | 6 | } |
783 | | } |
784 | | |
785 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
786 | | pub struct RrlConfig { |
787 | | pub enabled: bool, |
788 | | pub window_seconds: u64, |
789 | | pub slip: u32, |
790 | | pub qps_threshold: u64, |
791 | | } |
792 | | |
793 | | impl Default for RrlConfig { |
794 | 6 | fn default() -> Self { |
795 | 6 | RrlConfig { |
796 | 6 | enabled: true, |
797 | 6 | window_seconds: 5, |
798 | 6 | slip: 2, |
799 | 6 | qps_threshold: 50, |
800 | 6 | } |
801 | 6 | } |
802 | | } |
803 | | |
804 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
805 | | pub struct ZoneConfig { |
806 | | pub name: String, |
807 | | #[serde(rename = "type")] |
808 | | pub kind: ZoneType, |
809 | | #[serde(default)] |
810 | | pub file: Option<String>, |
811 | | #[serde(default)] |
812 | | pub notify: Option<bool>, |
813 | | #[serde(default)] |
814 | | pub notify_acl: Option<String>, |
815 | | #[serde(default)] |
816 | | pub allow_transfer_acl: Option<String>, |
817 | | #[serde(default)] |
818 | | pub allow_update_acl: Option<String>, |
819 | | #[serde(default)] |
820 | | pub axfr_tsig_key: Option<String>, |
821 | | |
822 | | // Slave-specific |
823 | | #[serde(default)] |
824 | | pub masters: Vec<String>, |
825 | | |
826 | | // Inline zone |
827 | | #[serde(default)] |
828 | | pub inline: Option<bool>, |
829 | | #[serde(default)] |
830 | | pub records: Vec<ZoneRecord>, |
831 | | |
832 | | // Forward-specific |
833 | | #[serde(default)] |
834 | | pub forwarders: Vec<String>, |
835 | | #[serde(default)] |
836 | | pub forward_policy: Option<String>, |
837 | | } |
838 | | |
839 | | impl Default for ZoneConfig { |
840 | 1 | fn default() -> Self { |
841 | 1 | ZoneConfig { |
842 | 1 | name: String::new(), |
843 | 1 | kind: ZoneType::Master, |
844 | 1 | file: None, |
845 | 1 | notify: Some(false), |
846 | 1 | notify_acl: None, |
847 | 1 | allow_transfer_acl: None, |
848 | 1 | allow_update_acl: None, |
849 | 1 | axfr_tsig_key: None, |
850 | 1 | masters: Vec::new(), |
851 | 1 | inline: Some(false), |
852 | 1 | records: Vec::new(), |
853 | 1 | forwarders: Vec::new(), |
854 | 1 | forward_policy: None, |
855 | 1 | } |
856 | 1 | } |
857 | | } |
858 | | |
859 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
860 | | #[serde(rename_all = "lowercase")] |
861 | | #[derive(PartialEq)] |
862 | | pub enum ZoneType { |
863 | | Master, |
864 | | Slave, |
865 | | Forward, |
866 | | Stub, |
867 | | } |
868 | | |
869 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
870 | | pub struct ZoneRecord { |
871 | | pub name: String, |
872 | | pub ttl: Option<u32>, |
873 | | pub class: Option<String>, |
874 | | #[serde(rename = "type")] |
875 | | pub r#type: String, |
876 | | pub rdata: String, |
877 | | #[serde(default)] |
878 | | pub priority: Option<u16>, |
879 | | } |
880 | | |
881 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
882 | | pub struct TsigKey { |
883 | | pub name: String, |
884 | | pub algorithm: String, |
885 | | pub secret: String, // TODO: base64 encoded - do not keep in plaintext in production |
886 | | } |
887 | | |
888 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
889 | | pub struct AxfrConfig { |
890 | | pub enabled: bool, |
891 | | pub max_concurrent_transfers: usize, |
892 | | pub transfer_timeout_secs: u64, |
893 | | } |
894 | | |
895 | | impl Default for AxfrConfig { |
896 | 6 | fn default() -> Self { |
897 | 6 | AxfrConfig { |
898 | 6 | enabled: true, |
899 | 6 | max_concurrent_transfers: 4, |
900 | 6 | transfer_timeout_secs: 120, |
901 | 6 | } |
902 | 6 | } |
903 | | } |
904 | | |
905 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
906 | | pub struct DnssecConfig { |
907 | | pub enabled: bool, |
908 | | pub auto_sign: bool, |
909 | | pub default_algo: String, |
910 | | pub kasp_file: Option<String>, |
911 | | } |
912 | | |
913 | | impl Default for DnssecConfig { |
914 | 6 | fn default() -> Self { |
915 | 6 | DnssecConfig { |
916 | 6 | enabled: false, |
917 | 6 | auto_sign: false, |
918 | 6 | default_algo: "RSASHA256".to_string(), |
919 | 6 | kasp_file: None, |
920 | 6 | } |
921 | 6 | } |
922 | | } |
923 | | |
924 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
925 | | pub struct PolicyConfig { |
926 | | #[serde(default)] |
927 | | pub deny_domains: Vec<String>, |
928 | | } |
929 | | |
930 | | impl Default for PolicyConfig { |
931 | 5 | fn default() -> Self { |
932 | 5 | PolicyConfig { |
933 | 5 | deny_domains: Vec::new(), |
934 | 5 | } |
935 | 5 | } |
936 | | } |
937 | | |
938 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
939 | | pub struct AmplificationMitigationConfig { |
940 | | pub drop_fragments: bool, |
941 | | pub max_response_size_udp: usize, |
942 | | } |
943 | | |
944 | | impl Default for AmplificationMitigationConfig { |
945 | 5 | fn default() -> Self { |
946 | 5 | AmplificationMitigationConfig { |
947 | 5 | drop_fragments: true, |
948 | 5 | max_response_size_udp: 4096, |
949 | 5 | } |
950 | 5 | } |
951 | | } |
952 | | |
953 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
954 | | pub struct TuningConfig { |
955 | | pub socket_recv_buffer_bytes: usize, |
956 | | pub socket_send_buffer_bytes: usize, |
957 | | pub max_label_length: usize, |
958 | | pub max_domain_length: usize, |
959 | | } |
960 | | |
961 | | impl Default for TuningConfig { |
962 | 5 | fn default() -> Self { |
963 | 5 | TuningConfig { |
964 | 5 | socket_recv_buffer_bytes: 262_144, |
965 | 5 | socket_send_buffer_bytes: 262_144, |
966 | 5 | max_label_length: 63, |
967 | 5 | max_domain_length: 253, |
968 | 5 | } |
969 | 5 | } |
970 | | } |
971 | | |
972 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
973 | | pub struct ViewConfig { |
974 | | pub name: String, |
975 | | pub acl: String, |
976 | | #[serde(default)] |
977 | | pub zones: Vec<ViewZone>, |
978 | | } |
979 | | |
980 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
981 | | pub struct ViewZone { |
982 | | pub name: String, |
983 | | pub file: String, |
984 | | } |
985 | | |
986 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
987 | | pub struct MonitoringConfig { |
988 | | pub enable_query_logging: bool, |
989 | | pub query_log_path: String, |
990 | | pub log_query_qps: u64, |
991 | | } |
992 | | |
993 | | impl Default for MonitoringConfig { |
994 | 5 | fn default() -> Self { |
995 | 5 | MonitoringConfig { |
996 | 5 | enable_query_logging: false, |
997 | 5 | query_log_path: "/var/log/scloud-dns/queries.log".to_string(), |
998 | 5 | log_query_qps: 1000, |
999 | 5 | } |
1000 | 5 | } |
1001 | | } |
1002 | | |
1003 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1004 | | pub struct DynUpdateConfig { |
1005 | | pub zone: String, |
1006 | | pub acl: String, |
1007 | | pub tsig_key: Option<String>, |
1008 | | pub allow: bool, |
1009 | | } |
1010 | | |
1011 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1012 | | pub struct LimitsConfig { |
1013 | | pub max_udp_packet_size: usize, |
1014 | | pub max_queries_per_minute_per_ip: u64, |
1015 | | pub max_tcp_sessions_per_ip: usize, |
1016 | | } |
1017 | | |
1018 | | impl Default for LimitsConfig { |
1019 | 6 | fn default() -> Self { |
1020 | 6 | LimitsConfig { |
1021 | 6 | max_udp_packet_size: 4096, |
1022 | 6 | max_queries_per_minute_per_ip: 1000, |
1023 | 6 | max_tcp_sessions_per_ip: 8, |
1024 | 6 | } |
1025 | 6 | } |
1026 | | } |